Modelos de Suavización Exponencial#

import pandas as pd
url = 'https://raw.githubusercontent.com/evgomez98/wind_speed/main/wind_dataset.csv'
df = pd.read_csv(url)
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
sns.set_style("darkgrid")

Creamos la función firstsmooth para aplicar suavización exponencial simple a la serie de tiempo y, asignando un peso decreciente a los valores antiguos según el factor de suavización \(\lambda\).

def firstsmooth(y, lambda_, start=None):
    ytilde = y.copy()
    if start is None:
        start = y[0]
    ytilde[0] = lambda_ * y[0] + (1 - lambda_) * start
    for i in range(1, len(y)):
        ytilde[i] = lambda_ * y[i] + (1 - lambda_) * ytilde[i - 1]
    return ytilde

Suavizamos la serie completa de la velocidad del viento y graficamos.

w_smooth1 = firstsmooth(y=df['WIND'], lambda_=0.3)
import plotly.graph_objects as go
fig = go.Figure()

fig.add_trace(go.Scatter(
    x=df['DATE'], 
    y=df['WIND'], 
    mode='markers',  
    name='Wind Speed', 
    marker=dict(color='black', size=3)  
))
fig.add_trace(go.Scatter(
    x=df['DATE'], 
    y=w_smooth1, 
    mode='lines',  
    name='SES λ=0.3', 
    line=dict(color='blue') 
))
fig.update_layout(
    title="Wind Speed vs Date",
    xaxis_title="Date",
    yaxis_title="Wind Speed",
    template='plotly_white'
)
fig.update_layout(legend=dict(
    x=0.05,
    y=0.95,  
    traceorder="normal"
))
fig.show()

La gráfica nos muestra que la suavización exponencial de primer orden no alcanza a captar de manera optima el comportamiento de la serie. Adicionalemnte aplicamos una función que nos devuelve las metricas de error, para evaluar la precisión del modelo.

def measacc_fs(y, lambda_):
    out = firstsmooth(y, lambda_)
    T = len(y)
    yh = y.copy().values
    out = pd.concat([pd.Series([y[0]]), out.iloc[:-1]], ignore_index=True).values
    prederr = yh - out
    SSE = sum(prederr**2)
    MAPE = 100 * sum(abs(prederr / yh)) / T
    MAE = sum(abs(prederr)) / T
    MSD = sum(prederr**2) / T
    ret1 = pd.DataFrame({
        "SSE": [SSE],
        "MAPE": [MAPE],
        "MAE": [MAE],
        "MSD": [MSD]
    })
    ret1.reset_index(drop=True, inplace=True)
    return ret1
measacc_fs(df['WIND'], 0.5)
SSE MAPE MAE MSD
0 122208.663778 inf 3.379654 18.589696

Tenemos que la suma de los errores cuadráticos es de 122208.6637, es decir que se obtuvo una cantidad significativa de error acumulado, indicando que el modelo está prediciendo con poca precisión. El error porcentual absoluto medio infinito sugiere que hay valores que no permiten calcular el error porcentual de manera adecuada.En cuanto al error absoluto medio, este significa que, en promedio, las predicciones están equivocadas por aproximadamente 3.38 nudos. Por ultimo la desviación cuadrática media, con un valor de 18.59 sugiere que los errores tienden a ser significativos, con algunos errores bastante grandes.

Así como se aplicó la suavización de primer orden, realizaremos la suavización de segundo orden para el conjunto de WIND. Haremos uso de la misma función, pero esta vez sobre el conjubti de datos suavizado:

w_smooth2 = firstsmooth(y=w_smooth1, lambda_=0.5)
w_hat = 2 * w_smooth1 - w_smooth2
fig = go.Figure()

fig.add_trace(go.Scatter(
    x=df['DATE'], 
    y=df['WIND'], 
    mode='markers',  
    name='Wind Speed', 
    marker=dict(color='black', size=3)  
))
fig.add_trace(go.Scatter(
    x=df['DATE'], 
    y=w_hat, 
    mode='lines',  
    name='SES λ=0.5', 
    line=dict(color='blue') 
))
fig.update_layout(
    title="Wind Speed vs Date",
    xaxis_title="Date",
    yaxis_title="Wind Speed",
    template='plotly_white'
)
fig.update_layout(legend=dict(
    x=0.05,
    y=0.95,  
    traceorder="normal"
))
fig.show()

Graficamente, podemos intuir que la segunda suavización proporciona mayor precisión en tanto que parece ajustarse de mejor manera al comportamiento de la serie.

measacc_fs(w_smooth1, 0.5)
SSE MAPE MAE MSD
0 18105.323473 14.486066 1.316461 2.75408

Las metricas de error indican que suma de los errores cuadráticos es de 41027.0401, es decir que se obtuvo una cantidad significativa de error acumulado. Sin embargo, es aproximadamente un tercio de la suma de los errores cuadráticos de la SES. El error porcentual absoluto medio de 22.82% sugiere que el modelo tiene un rendimiento moderado. En cuanto al error absoluto medio, este significa que, en promedio, las predicciones están desviadas por aproximadamente 1.97 nudos. Por ultimo la desviación cuadrática media, con un valor de 6.24 se considera bajo, indicativo de que si bien el modelo presenta errores, no son significativamente altos.

A continuación dividimos el conjunto de datos:

tau_test = 365
tau_val  = 365

train = df['WIND'][:-(tau_val + tau_test)].copy()
val   = df['WIND'][-(tau_val + tau_test):-tau_test].copy()
test  = df['WIND'][-tau_test:].copy()

print(f"Train: {len(train)}, Validation: {len(val)}, Test: {len(test)}")
Train: 5844, Validation: 365, Test: 365

Factor de descuento#

Con el propósito de ajustar el modelo al comportamiento y variabilidad de los datos y sus errores a través del tiempo, se implementa el factor de descuento. Lo quese busca es que este factor varíe a lo largo del tiempo, permitiendo que el modelo se ajuste más o menos rápidamente según los cambios en los datos.

import numpy as np

def tlsmooth(y, delta_, y_tilde_start=None, lambda_start=1):
    T = len(y)
    
    Qt = np.zeros(T)
    Dt = np.zeros(T)
    y_tilde = np.zeros(T)
    lambd = np.zeros(T)
    err = np.zeros(T)
    
    lambd[0] = lambda_start
    if y_tilde_start is None:
        y_tilde[0] = y[0]
    else:
        y_tilde[0] = y_tilde_start
    
    for i in range(1, T):
        err[i] = y[i] - y_tilde[i-1]
        Qt[i] = delta_ * err[i] + (1 - delta_) * Qt[i-1]
        Dt[i] = delta_ * abs(err[i]) + (1 - delta_) * Dt[i-1]
        lambd[i] = abs(Qt[i] / Dt[i])
        y_tilde[i] = lambd[i] * y[i] + (1 - lambd[i]) * y_tilde[i-1]
    
    return np.column_stack((y_tilde, lambd, err, Qt, Dt))
ws = tlsmooth(train, 0.9)
last_fc = 365
fig = go.Figure()

fig.add_trace(go.Scatter(
    x=df['DATE'][-last_fc:], 
    y=df['WIND'][-last_fc:], 
    mode='markers',
    name='Wind Speed', 
    marker=dict(color='black', size=8) 
))
fig.add_trace(go.Scatter(
    x=df['DATE'][-last_fc:], 
    y=ws[-last_fc:, 0], 
    mode='lines',  
    name='TL Smoother',  
    line=dict(color='blue')  
))
fig.add_trace(go.Scatter(
    x=df['DATE'][-last_fc:], 
    y=w_smooth1[-last_fc:], 
    mode='lines', 
    name='Exponential Smoother',  
    line=dict(color='red') 
))
fig.update_layout(
    title="Wind Speed vs Date",
    xaxis_title="Date",
    yaxis_title="Wind Speed",
    template='plotly_white'
)

fig.update_layout(legend=dict(
    x=0.05,  
    y=0.95, 
    traceorder="normal"
))

fig.show()

Se observa como el modelo adaptativo tiene mayor capacidad para capturar el comportamiento de los datos. Pronosticando con mejor precisión los máximos y mínimos.

def c_metrics(y_real, y_pred):
    err_abs = np.abs(y_real - y_pred)
    err_rel = np.abs((y_real - y_pred) / y_real)

    MAPE = 100 * sum(abs(y_pred / y_real)) / len(y_pred)
    SSE = np.sum((y_real - y_pred)**2)
    MAE = np.mean(err_abs)
    MSD = np.mean((y_real - y_pred)**2)
    
    return {
        "SSE": SSE,
        "MAE": MAE,
        "MAPE": MAPE,
        "MSD": MSD
    }

ws = tlsmooth(df['WIND'], delta_=0.9)
y_tilde = ws[:, 0]
metricas = c_metrics(df['WIND'], y_tilde)

print(metricas)
{'SSE': 2107.053763072044, 'MAE': 0.34826111643796515, 'MAPE': inf, 'MSD': 0.32051319791178035}

Selección de \(\lambda\)#

import numpy as np 

Para la selección del \(\lambda\) se creea una función que automatiza la elección de este parámetro condicionandolo a la suma de los errores cuadráticos minima. Se crea un vector para \(\lambda\) desde el 0.1, que vaya de 0.1 en 0.1 hasta 1.

lambda_vec = np.arange(0.1, 1.0, 0.1)
def sse_speed(sc):
    return measacc_fs(train, sc)['SSE'].values[0]
sse_vec = pd.Series()
for lambda_ in lambda_vec:
    sse_vec.loc[len(sse_vec)] = sse_speed(lambda_)
opt_lambda = sse_vec.min()
import plotly.graph_objects as go

fig = go.Figure()

fig.add_trace(go.Scatter(
    x=lambda_vec, 
    y=sse_vec, 
    mode='lines+markers', 
    name='SSE vs Lambda',
    line=dict(color='blue'),  
    marker=dict(color='blue', size=8)  
))
fig.add_shape(
    type='line',
    x0=lambda_vec[sse_vec.idxmin()], x1=lambda_vec[sse_vec.idxmin()],
    y0=min(sse_vec), y1=max(sse_vec),
    line=dict(color='red', dash='dash'), 
    name='Min SSE'
)
fig.update_layout(
    title="SSE vs. Lambda",
    xaxis_title="Lambda (λ)",
    yaxis_title="SSE",
    template='plotly_white', 
    height=500, 
    width=700
)
fig.show()

Los resultados se aprecian graficamente, indicando que el valor optimo para \(\lambda\) es de 0.5. Cabe mencionar, que este valor está fuera del rango que recomendado por la literatura (0.1 a 0.4).

Las metricas de error indican que suma de los errores cuadráticos es de 2107.0537, es decir que se obtuvo una cantidad baja de error acumulado. El error porcentual absoluto medio infinito sugiere que hay valores que no permiten calcular el error porcentual de manera adecuada. En cuanto al error absoluto medio, este significa que, en promedio, las predicciones están desviadas por aproximadamente 0.35 nudos. Por ultimo la desviación cuadrática media, con un valor de 0.3205 se considera bajo, indicativo de que si bien el modelo presenta errores, no son significativamente altos.

Suavización exponencial simple#

from matplotlib import pyplot as plt
import seaborn as sns
import statsmodels.api as sm
from sklearn.metrics import mean_absolute_error
import itertools
import warnings
from statsmodels.tools.sm_exceptions import ConvergenceWarning
warnings.simplefilter('ignore', ConvergenceWarning)
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.holtwinters import SimpleExpSmoothing
from statsmodels.tsa.seasonal import seasonal_decompose
import statsmodels.tsa.api as smt

Creamos una función que retorne un gráfico para la serie seccionada y las predicciones:

def plot_model(train, val, test, y_pred, title):
    mae = mean_absolute_error(test, y_pred)
    fig = go.Figure()
    
    fig.add_trace(go.Scatter(
        x=train.index, 
        y=train, 
        mode='lines', 
        name='Train', 
        line=dict(color='blue')
    ))
    fig.add_trace(go.Scatter(
        x=val.index, 
        y=val, 
        mode='lines', 
        name='Validation', 
        line=dict(color='orange')
    ))
    fig.add_trace(go.Scatter(
        x=test.index, 
        y=test, 
        mode='lines', 
        name='Test', 
        line=dict(color='green')
    ))
    fig.add_trace(go.Scatter(
        x=y_pred.index, 
        y=y_pred, 
        mode='lines', 
        name='Prediction', 
        line=dict(color='red', dash='dash') 
    ))

    fig.update_layout(
        title=f"{title}, MAE: {round(mae, 2)}",
        xaxis_title="",
        yaxis_title="",
        template='plotly_white',
        legend=dict(
            x=0.05,
            y=0.95
        )
    )

    fig.show()

Asimismo se define la función para optimizar la SES a través de la selección del mejor \(\alpha\) y el mejor MAE de manera simultanea:

def ses_optimizer(train, val, alphas, step):

    best_alpha, best_mae = None, float("inf")

    for alpha in alphas:
        ses_model = SimpleExpSmoothing(train).fit(smoothing_level=alpha, optimized=False)
        y_pred = ses_model.forecast(step)
        mae = mean_absolute_error(val, y_pred)

        if mae < best_mae:
            best_alpha, best_mae = alpha, mae

    return best_alpha, best_mae
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from statsmodels.stats.diagnostic import acorr_ljungbox
from scipy.stats import normaltest
import numpy as np
import pandas as pd
from statsmodels.tsa.holtwinters import SimpleExpSmoothing

def ses_model_tuning(train, val, test, step, title="Model Tuning - Single Exponential Smoothing"):
    alphas = np.arange(0.8, 1, 0.01)
  
    best_alpha, best_mae = ses_optimizer(train, val, alphas, step=step)
    train_val = pd.concat([train, val])
    final_model = SimpleExpSmoothing(train_val).fit(smoothing_level=best_alpha, optimized=False)
    
    y_pred = final_model.forecast(step)

    mae = mean_absolute_error(test, y_pred)
    sse = np.sum((test - y_pred) ** 2)
    mape = np.mean(np.abs((test - y_pred) / test)) * 100
    msd = mean_squared_error(test, y_pred)
    r2 = r2_score(test, y_pred)  
    ljung_box = acorr_ljungbox(test - y_pred, lags=[90], return_df=True)

    normality_test_stat, normality_p_value = normaltest(test - y_pred)
    
    df_acc = pd.DataFrame({'MAE': [mae],
                           'SSE': [sse],
                           'MAPE': [mape],
                           'MSD': [msd],
                           'R2': [r2],
                           'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                           'Normalidad (p-value)': [normality_p_value]},
                           index=[title])
    
    plot_model(train, val, test, y_pred, title)

    return df_acc
ses_model_tuning(train, val, test, step=tau_test)
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
Model Tuning - Single Exponential Smoothing 7.058714 24398.471706 171.303636 66.845128 -1.442531 1.143209e-50 0.000322

El MAE, con un valor de 7.06, sugiere que las predicciones están, en promedio, desviadas por 7.06 nudos de los valores reales. Adicionalmente, la SSE de 24,398.47 revela un error acumulado considerable, mientras que el MAPE de 171.30% resalta una elevada inexactitud porcentual, lo que indica que el modelo es poco confiable para predecir de manera precisa. La desviación cuadrática media, de 66.85, confirma la magnitud significativa de los errores en el modelo. Además, el coeficiente negativo (-1.44) señala un ajuste deficiente, ya que las predicciones son menos precisas que una media simple. Los p-valores de las pruebas de Ljung-Box (1.14e-50) y de normalidad (0.000322) sugieren que los residuos presentan correlaciones significativas y no siguen una distribución normal, lo cual afecta la aleatoriedad y la validez de los resultados. Todas stas métricas sugieren que el modelo no logra capturar adecuadamente los patrones serie temporal.

Suavización exponencial doble#

def des_optimizer(train, val, alphas, betas, trend, step):

    best_alpha, best_beta, best_mae = None, None, float("inf")

    for alpha in alphas:
        for beta in betas:
            des_model = ExponentialSmoothing(train, trend=trend).fit(smoothing_level=alpha, smoothing_slope=beta)
            y_pred = des_model.forecast(step)
            mae = mean_absolute_error(val, y_pred)
            if mae < best_mae:
                best_alpha, best_beta, best_mae = alpha, beta, mae

    return best_alpha, best_beta, best_mae
def des_model_tuning(train , val, test, step, trend, title="Model Tuning - Double Exponential Smoothing"):
    
    alphas = np.arange(0.01, 1, 0.10)
    betas  = np.arange(0.01, 1, 0.10)
    best_alpha, best_beta, best_mae = des_optimizer(train, val, alphas, betas, trend=trend, step=step)

    train_val = pd.concat([train, val])
    final_model = ExponentialSmoothing(train_val, trend=trend).fit(smoothing_level=best_alpha, smoothing_slope=best_beta)
    y_pred = final_model.forecast(step)
    
    mae = mean_absolute_error(test, y_pred)
    sse = np.sum((test - y_pred) ** 2)
    mape = np.mean(np.abs((test - y_pred) / test)) * 100
    msd = mean_squared_error(test, y_pred)
    r2 = r2_score(test, y_pred)  
    
    ljung_box = acorr_ljungbox(test - y_pred, lags=[90], return_df=True)

    normality_test_stat, normality_p_value = normaltest(test - y_pred)
    
    df_acc = pd.DataFrame({'MAE': [mae],
                           'SSE': [sse],
                           'MAPE': [mape],
                           'MSD': [msd],
                           'R2': [r2],
                           'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                           'Normalidad (p-value)': [normality_p_value]},
                           index=[title])
    
    plot_model(train, val, test, y_pred, title)

    return df_acc
des_model_tuning(train, val, test, step=tau_test, trend=None)
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
Model Tuning - Double Exponential Smoothing 6.857008 23150.686094 167.00335 63.426537 -1.317615 1.143209e-50 0.000322

El MAE, con un valor de 6.86, sugiere que las predicciones están, en promedio, desviadas por 6.86 nudos de los valores reales. La SSE, que se sitúa en 23,150.69, indica un error acumulado considerable, lo que refuerza la idea de que el modelo no está ajustando adecuadamente los datos. Por otro lado, el MAPE de 167.00% evidencia una deficiencia porcentual notable, lo que sugiere que el modelo presenta una imprecisión significativa en sus predicciones. La desviación cuadrática media, con un valor de 63.43, confirma la magnitud considerable de los errores en el modelo. El coeficiente R², que también es negativo (-1.32), indica un ajuste deficiente, sugiriendo que las predicciones son menos precisas que el promedio simple. Finalmente, los p-valores de las pruebas de Ljung-Box (1.14e-50) y de normalidad (0.000322) apuntan a la presencia de correlaciones significativas en los residuos y a la falta de una distribución normal, lo que compromete la aleatoriedad y la validez de los resultados. En conjunto, estas métricas reflejan que el modelo de suavización exponencial doble no logra captar adecuadamente los patrones presentes en la serie temporal.

Suavización exponencial triple#

def tes_optimizer(train, val, abg, trend, seasonal,  seasonal_periods, step):
    
    best_alpha, best_beta, best_gamma, best_mae = None, None, None, float("inf")
    for comb in abg:
        tes_model = ExponentialSmoothing(train, trend=trend, seasonal=seasonal, seasonal_periods=seasonal_periods).\
            fit(smoothing_level=comb[0], smoothing_slope=comb[1], smoothing_seasonal=comb[2])
        y_pred = tes_model.forecast(step)
        mae = mean_absolute_error(val, y_pred)
        if mae < best_mae:
            best_alpha, best_beta, best_gamma, best_mae = comb[0], comb[1], comb[2], mae

    return best_alpha, best_beta, best_gamma, best_mae
def tes_model_tuning(train, val, test, step, trend, seasonal, seasonal_periods, title="Model Tuning - Triple Exponential Smoothing"):
    
    alphas = betas = gammas = np.arange(0.10, 1, 0.10)
    abg = list(itertools.product(alphas, betas, gammas))
    best_alpha, best_beta, best_gamma, best_mae = tes_optimizer(train, val, abg=abg, trend=trend, seasonal=seasonal, seasonal_periods=seasonal_periods, step=step)
    
    final_model = ExponentialSmoothing(train, trend=trend, seasonal=seasonal).fit(smoothing_level=best_alpha, smoothing_slope=best_beta, smoothing_seasonal=best_gamma)
    y_pred = final_model.forecast(step + step)[-step:]

    mae = mean_absolute_error(test, y_pred)
    sse = np.sum((test - y_pred) ** 2)
    mape = np.mean(np.abs((test - y_pred) / test)) * 100
    msd = mean_squared_error(test, y_pred)
    r2 = r2_score(test, y_pred) 
    
    ljung_box = acorr_ljungbox(test - y_pred, lags=[90], return_df=True)
    
    normality_test_stat, normality_p_value = normaltest(test - y_pred)

    df_acc = pd.DataFrame({'MAE': [mae],
                           'SSE': [sse],
                           'MAPE': [mape],
                           'MSD': [msd],
                           'R2': [r2],
                           'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                           'Normalidad (p-value)': [normality_p_value]},
                           index=[title])
    
    plot_model(train, val, test, y_pred, title)

    return df_acc
# best_alpha, best_beta, best_gamma, best_mae = tes_model_tuning(train, val, test, step=tau_test, trend=None, seasonal='add', seasonal_periods=365)
El modelo de Triple Suavizado Exponencial no se ejecuta por el riesgo de sobrecalentamiento del equipo.

Modelo de Suavización Exponencial Adaptativo#

Teniendo en cuenta que inicialmente el modelo con factor de descuento mostraba mejor ajuste, se decide entrenarlo y poner a prueba los resultado con los subconjutos de datos de velocidad del viento.

index = 'TLsmooth train Δ = 0.8'
delta_ = 0.8
train_result = tlsmooth(train, delta_)

train_pred = train_result[:, 0]  

mae = mean_absolute_error(train, train_pred)
sse = np.sum((train - train_pred) ** 2)
mape = np.mean(np.abs((train - train_pred) / train)) * 100
msd = mean_squared_error(train, train_pred)
r2 = r2_score(train, train_pred)  

ljung_box = acorr_ljungbox(train - train_pred, lags=[90], return_df=True)
   
normality_test_stat, normality_p_value = normaltest(train - train_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
TLsmooth train Δ = 0.8 0.646954 5820.450289 inf 0.99597 0.959359 3.612900e-94 1.851872e-115
index = 'TLsmooth val Δ = 0.8'

val = val.reset_index(drop=True)

val_result = tlsmooth(val, delta_)
val_pred = val_result[:, 0]  

mae = mean_absolute_error(val, val_pred)
sse = np.sum((val-val_pred) ** 2)
mape = np.mean(np.abs((val-val_pred) / val)) * 100
msd = mean_squared_error(val, val_pred)
r2 = r2_score(val, val_pred) 

ljung_box = acorr_ljungbox(val-val_pred, lags=[90], return_df=True)

normality_test_stat, normality_p_value = normaltest(val- val_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
TLsmooth val Δ = 0.8 0.641642 371.865318 6.046808 1.018809 0.956057 0.041971 1.511232e-08

Las metricas de error indican que suma de los errores cuadráticos es de 317.87, es decir que se obtuvo una cantidad baja de error. El error porcentual absoluto medio de 6.05% sugiere un modelo altamente preciso. En cuanto al error absoluto medio, este significa que, en promedio, las predicciones están desviadas por aproximadamente 0.64 nudos. Por ultimo la desviación cuadrática media, con un valor de 1.02 se considera bastante baja, indicativo de que el modelo presenta un buen ajuste.

index = 'TLsmooth test Δ = 0.8'

test = test.reset_index(drop=True)
test_result = tlsmooth(test, delta_)

test_pred = test_result[:, 0]  

mae = mean_absolute_error(test, test_pred)
sse = np.sum((test - test_pred) ** 2)
mape = np.mean(np.abs((test - test_pred) / test)) * 100
msd = mean_squared_error(test, test_pred)
r2 = r2_score(test,test_pred) 

ljung_box = acorr_ljungbox(test - test_pred, lags=[90], return_df=True)

normality_test_stat, normality_p_value = normaltest(test - test_pred)

df_acc = pd.DataFrame({'MAE': [mae],
                    'SSE': [sse],
                    'MAPE': [mape],
                    'MSD': [msd],
                    'R2': [r2],
                    'Ljung-Box (p-value)': [ljung_box['lb_pvalue'].iloc[0]],
                    'Normalidad (p-value)': [normality_p_value]},
                    index= [index])
df_acc.head()
MAE SSE MAPE MSD R2 Ljung-Box (p-value) Normalidad (p-value)
TLsmooth test Δ = 0.8 0.65569 399.159125 8.631877 1.093587 0.96004 0.002216 4.763272e-10

El MAE, que presenta un valor de 0.66, sugiere que las predicciones están, en promedio, desviadas por 0.66 nudos de los valores reales, lo que indica una precisión aceptable en el modelo. La SSE, con un total de 399.16, refleja un error acumulado relativamente bajo, lo que refuerza la idea de que el modelo ajusta razonablemente los datos. El MAPE de 8.63% indica que el modelo tiene una deficiencia porcentual bastante aceptable, lo que sugiere una buena capacidad predictiva. La desviación cuadrática media, de 1.09, también es baja, lo que indica que los errores en las predicciones son mínimos. En cuanto al coeficiente R², con un valor de 0.96, indica un ajuste excelente, sugiriendo que el modelo captura bien la variabilidad de los datos. Sin embargo, los p-valores de las pruebas de Ljung-Box (0.0022) y de normalidad (4.76e-10) sugieren que hay correlaciones significativas en los residuos y que estos no siguen una distribución normal, lo que podría afectar la aleatoriedad y la validez de los resultados. Es decir que aunque el modelo TLsmooth muestra un buen desempeño en general, existen no es completamente adecuado.

def plot_model(train, val, test, test_pred, title):
  
    train = np.array(train)
    val = np.array(val)
    test = np.array(test)
    test_pred = np.array(test_pred)

   
    train_index = np.arange(0, len(train))
    val_index = np.arange(len(train), len(train) + len(val))
    test_index = np.arange(len(train) + len(val), len(train) + len(val) + len(test))
    test_pred_index = test_index  

    fig = go.Figure()
    fig.add_trace(go.Scatter(x=train_index, y=train, mode='lines', name='Train', line=dict(color='blue')))
    fig.add_trace(go.Scatter(x=val_index, y=val, mode='lines', name='Validation', line=dict(color='orange')))
    fig.add_trace(go.Scatter(x=test_index, y=test, mode='lines', name='Test', line=dict(color='green')))
    fig.add_trace(go.Scatter(x=test_pred_index, y=test_pred, mode='lines', name='Prediction', line=dict(color='red', dash='dash')))

    fig.update_layout(
        title=title,
        xaxis_title="Observaciones",
        yaxis_title="Valores",
        legend=dict(x=0, y=1, traceorder="normal"),
        plot_bgcolor='rgba(0,0,0,0)',
        width=1000, height=600
    )

    fig.show()
plot_model(train, val, test, test_pred, 'TL Model Δ = 0.8')

Por último, observamos el comportamiento del conjunto de datos y el ajuste de los mismos a las prediciones realizadas.